﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using Nsf.Samples.RetryActivitySample.Infrastructure.Common.FaultDetails;
using Nsf.Samples.RetryActivitySample.Infrastructure.InternalInterfaces;
using Nsf.Samples.RetryActivitySample.Infrastructure.Logger;
using Nsf.Samples.RetryActivitySample.Infrastructure.Utils;

namespace Nsf.Samples.RetryActivitySample.Infrastructure.ServiceBase
{
    public class ServiceErrorsHandler : IErrorHandler
    {
        #region Constants

        private const String FAULT_CONTRACT_NOT_FOUND_TEMPLATE = "Контракт типа '{0}', предоставляющего информацию о сбое, не был объявлен для вызываемой операции '{1}'.{2}Дополнительная информация:{2}{3}";
        private const String EXCEPTION_CONVERTER_NOT_FOUND_TEMPLATE = "Не удалось получить информацию о сбое по исключению '{0}'";

        #endregion

        #region Internal State

        private readonly ILogger _logger;

        #endregion

        #region Constructors

        public ServiceErrorsHandler(ILogger logger)
        {
            _logger = logger;
        }

        #endregion

        #region Protected Methods

        protected virtual void LogException(Exception ex)
        {
            if (ex is FaultException<ValidationFaultDetails>)
            {
                var preparedError = (FaultException<ValidationFaultDetails>)ex;
                var errorDescription = String.Format("{0}{1}{2}", preparedError.Detail, Environment.NewLine, ExceptionHelper.GetStackInformation(ex));
                _logger.Warning(errorDescription, ex);
            }
            else if (ex is FaultException<ProcessingFaultDetails>)
            {
                var preparedError = (FaultException<ProcessingFaultDetails>)ex;
                var errorDescription = String.Format("Во время выполнения операции произошла ошибка:{0}{1}{0}{2}", Environment.NewLine, preparedError.Detail.Details, ExceptionHelper.GetStackInformation(ex));
                _logger.Error(errorDescription, ex);
            }
            else if (ex is CommunicationException)
            {
                var errorDescription = ExceptionHelper.GetFullInformation(ex);
                if (ex.InnerException != null && ex.InnerException is SocketException)
                {
                    var socketException = (SocketException)ex.InnerException;
                    if (socketException.SocketErrorCode == SocketError.ConnectionReset)
                        _logger.Warning(errorDescription, ex);
                    else
                        _logger.Error(errorDescription, ex);
                }
                else
                {
                    _logger.Error(errorDescription, ex);
                }
            }
            else
            {
                var errorDescription = ExceptionHelper.GetFullInformation(ex);
                _logger.Error(errorDescription, ex);
            }
        }

        private OperationDescription GetCurrentOperationDescription()
        {
            var operationContext = OperationContext.Current;
            if (operationContext == null)
                return null;

            var hostDescription = operationContext.Host.Description;
            var serviceEndpoint = hostDescription.Endpoints.Find(operationContext.EndpointDispatcher.EndpointAddress.Uri);
            if (serviceEndpoint == null)
                return null;

            var serviceEndpointContractFullName = serviceEndpoint.Contract.Namespace + serviceEndpoint.Contract.Name + "/";
            var operationName = operationContext.IncomingMessageHeaders.Action.Replace(serviceEndpointContractFullName, "");
            return serviceEndpoint.Contract.Operations.Find(operationName);
        }

        private FaultDescription GetKnownFaultDescription(OperationDescription operationDescription, Type faultDetailsType)
        {
            var faultDescription = operationDescription.Faults.FirstOrDefault(obj => obj.DetailType == faultDetailsType) ??
                                   operationDescription.Faults.FirstOrDefault(obj => obj.DetailType.IsAssignableFrom(faultDetailsType));
            return faultDescription;
        }

        #endregion

        #region IErrorHandler Members

        public bool HandleError(Exception error)
        {
            // Логируем информацию об исключении
            LogException(error);
            return true;
        }

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            var operationDescription = GetCurrentOperationDescription();
            if (operationDescription == null)
            {
                _logger.Error("Не удалось получить описание текущей операции");
                return;
            }
            var errorType = error.GetType();
            // Если это известный сбой, то проверяем, объявлен ли его контракт для операции
            if (errorType.IsGenericType && errorType.GetGenericTypeDefinition() == typeof(FaultException<>))
            {
                var faultDetailType = errorType.GetGenericArguments()[0];
                if (!(GetKnownFaultDescription(operationDescription, faultDetailType) == null))
                    return;
                // Контракт не найден
                var errorMessage = String.Format(FAULT_CONTRACT_NOT_FOUND_TEMPLATE, faultDetailType.FullName, operationDescription.Name, Environment.NewLine, error);
                _logger.Error(errorMessage);
                // Создаем базовое исключение, содержащее всю информацию о прежнем исключении
                // Его и отдадим пользователю, если для базового исключения зарегистрирован сбой
                error = new Exception(errorMessage);
                errorType = error.GetType();
            }

            // Получаем информацию о сбое по исключению
            var faultDetails = ExceptionToFaultConverter.ConvertException(error);
            if (faultDetails == null)
            {
                _logger.Error(String.Format(EXCEPTION_CONVERTER_NOT_FOUND_TEMPLATE, errorType.FullName));
                return;
            }
            // Получаем описание сбоя по его типу и операции
            var faultDescription = GetKnownFaultDescription(operationDescription, faultDetails.GetType());
            if (faultDescription == null)
            {
                var errorMessage = String.Format(FAULT_CONTRACT_NOT_FOUND_TEMPLATE, faultDetails.GetType().FullName, operationDescription.Name, Environment.NewLine, error);
                _logger.Error(errorMessage);
                return;
            }
            // Создаем сообщение с информацией о сбое
            var faultCode = FaultCode.CreateSenderFaultCode(faultDescription.Name, faultDescription.Namespace);
            fault = Message.CreateMessage(version, faultCode, String.Empty, faultDetails, faultDescription.Action);
        }

        #endregion
    }
}
